package org.jeecg.common.util;

import com.google.common.collect.Multimap;
import io.minio.*;
import io.minio.errors.*;
import io.minio.http.Method;
import io.minio.messages.InitiateMultipartUploadResult;
import io.minio.messages.Part;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.filter.FileTypeFilter;
import org.jeecg.common.util.filter.StrAttackFilter;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Nonnull;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLDecoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

/**
 * minio文件上传工具类
 *
 * @author: jeecg-boot
 */
@Slf4j
public class MinioUtil {
    private static String minioUrl;
    private static String minioUrlExpose;
    private static String minioName;
    private static String minioPass;
    private static String bucketName;
    private static String privateBucketName;

    public static void setMinioUrl(String minioUrl) {
        MinioUtil.minioUrl = minioUrl;
    }

    public static void setMinioName(String minioName) {
        MinioUtil.minioName = minioName;
    }

    public static void setMinioPass(String minioPass) {
        MinioUtil.minioPass = minioPass;
    }

    public static void setBucketName(String bucketName) {
        MinioUtil.bucketName = bucketName;
    }

    public static void setMinioUrlExpose(String minioUrlExpose) {
        MinioUtil.minioUrlExpose = minioUrlExpose;
    }

    public static String getMinioUrlExpose() {
        return minioUrlExpose;
    }

    public static String getMinioUrl() {
        return minioUrl;
    }

    public static String getBucketName() {
        return bucketName;
    }

    public static void setPrivateBucketName(String privateBucketName) {
        MinioUtil.privateBucketName = privateBucketName;
    }

    public static String getPrivateBucketName() {
        return privateBucketName;
    }

    private static MinioClient minioClient = null;

    /**
     * 上传文件
     *
     * @param file
     * @return
     */
    public static String upload(MultipartFile file, String bizPath, String customBucket) throws Exception {
        String fileUrl = "";
        //update-begin-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符，防止攻击
        bizPath = StrAttackFilter.filter(bizPath);
        //update-end-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符，防止攻击

        //update-begin-author:liusq date:20210809 for: 过滤上传文件类型
        FileTypeFilter.fileTypeFilter(file);
        //update-end-author:liusq date:20210809 for: 过滤上传文件类型

        // update-begin-author:jiangtianyi date:20230504 for: 私有文件改造
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = servletRequestAttributes.getRequest();
        String privatelyOwned = request.getParameter("privatelyOwned");
        boolean tag = StringUtils.isNotEmpty(privatelyOwned) && "1".equals(privatelyOwned);
        String newBucket = tag ? privateBucketName : bucketName;
        // update-begin-author:jiangtianyi date:20230504 for: 私有文件改造

        if (oConvertUtils.isNotEmpty(customBucket)) {
            newBucket = customBucket;
        }
        try {
            initMinio(minioUrl, minioName, minioPass);
            // 检查存储桶是否已经存在
            if (minioClient.bucketExists(BucketExistsArgs.builder().bucket(newBucket).build())) {
                log.info("Bucket [{}] already exists.", newBucket);
            } else {
                // 创建一个名为ota的存储桶
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(newBucket).build());
                log.info("create a new bucket [{}] .", newBucket);
            }
            InputStream stream = file.getInputStream();
            // 获取文件名
            String orgName = file.getOriginalFilename();
            if ("".equals(orgName)) {
                orgName = file.getName();
            }
            String objectName = getObjectName(orgName, bizPath);

            // 使用putObject上传一个本地文件到存储桶中。
            if (objectName.startsWith(SymbolConstant.SINGLE_SLASH)) {
                objectName = objectName.substring(1);
            }
            PutObjectArgs objectArgs = PutObjectArgs.builder().object(objectName)
                    .bucket(newBucket)
//                    /* 20210601 update 根据file设置contentType by zhaowl start */
//                    //.contentType("application/octet-stream")
//                    .contentType(file.getContentType())
//                    /* 20210601 update 根据file设置contentType by zhaowl end */

                    // 20250317 解决文件预览xss安全漏洞；author：jiangtianyi； start
                    .contentType("application/octet-stream")
                    // 20250317 解决文件预览xss安全漏洞；author：jiangtianyi； end

                    .stream(stream, stream.available(), -1).build();
            minioClient.putObject(objectArgs);
            stream.close();
            // update-begin-author:jiangtianyi date:20230504 for: 私有文件改造
            fileUrl = tag ? objectName : minioUrlExpose + newBucket + "/" + objectName;
            // update-begin-author:jiangtianyi date:20230504 for: 私有文件改造
        } catch (Exception e) {
            //log.error(e.getMessage(), e);
            throw new JeecgBootException(e);
        }
        return fileUrl;
    }

    private static String getObjectName(String orgName, String bizPath) {
        orgName = CommonUtils.getFileName(orgName);
        return bizPath + "/"
                + (orgName.indexOf(".") == -1
                ? orgName + "_" + System.currentTimeMillis()
                : orgName.substring(0, orgName.lastIndexOf(".")) + "_" + System.currentTimeMillis() + orgName.substring(orgName.lastIndexOf("."))
        );
    }

    /**
     * 文件上传
     *
     * @param file
     * @param bizPath
     * @return
     */
    public static String upload(MultipartFile file, String bizPath) throws Exception {
        return upload(file, bizPath, null);
    }

    /**
     * 获取文件流
     *
     * @param bucketName
     * @param objectName
     * @return
     */
    public static InputStream getMinioFile(String bucketName, String objectName) {
        InputStream inputStream = null;
        try {
            initMinio(minioUrl, minioName, minioPass);
            GetObjectArgs objectArgs = GetObjectArgs.builder().object(objectName)
                    .bucket(bucketName).build();
            inputStream = minioClient.getObject(objectArgs);
        } catch (Exception e) {
            log.info("文件获取失败" + e.getMessage());
        }
        return inputStream;
    }

    /**
     * 删除文件
     *
     * @param bucketName
     * @param objectName
     * @throws Exception
     */
    public static void removeObject(String bucketName, String objectName) {
        try {
            initMinio(minioUrl, minioName, minioPass);
            RemoveObjectArgs objectArgs = RemoveObjectArgs.builder().object(objectName)
                    .bucket(bucketName).build();
            minioClient.removeObject(objectArgs);
        } catch (Exception e) {
            log.info("文件删除失败" + e.getMessage());
        }
    }

    /**
     * 获取文件外链
     *
     * @param bucketName
     * @param objectName
     * @param expires
     * @return
     */
    public static String getObjectUrl(String bucketName, String objectName, Integer expires) {
        initMinio(minioUrl, minioName, minioPass);
        try {
            //update-begin---author:liusq  Date:20220121  for：获取文件外链报错提示method不能为空，导致文件下载和预览失败----
            GetPresignedObjectUrlArgs objectArgs = GetPresignedObjectUrlArgs.builder().object(objectName)
                    .bucket(bucketName)
                    .expiry(expires).method(Method.GET).build();
            //update-begin---author:liusq  Date:20220121  for：获取文件外链报错提示method不能为空，导致文件下载和预览失败----
            String url = minioClient.getPresignedObjectUrl(objectArgs);
            return URLDecoder.decode(url, "UTF-8");
        } catch (Exception e) {
            log.info("文件路径获取失败" + e.getMessage());
        }
        return null;
    }

    /**
     * 初始化客户端
     *
     * @param minioUrl
     * @param minioName
     * @param minioPass
     * @return
     */
    private static MinioClient initMinio(String minioUrl, String minioName, String minioPass) {
        if (minioClient == null) {
            try {
                minioClient = MinioClient.builder()
                        .endpoint(minioUrl)
                        .credentials(minioName, minioPass)
                        .build();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return minioClient;
    }

    /**
     * 上传文件到minio
     *
     * @param stream
     * @param relativePath
     * @return
     */
    public static String upload(InputStream stream, String relativePath) throws Exception {
        initMinio(minioUrl, minioName, minioPass);
        if (minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
            log.info("Bucket already exists.");
        } else {
            // 创建一个名为ota的存储桶
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            log.info("create a new bucket.");
        }
        PutObjectArgs objectArgs = PutObjectArgs.builder().object(relativePath)
                .bucket(bucketName)
                .contentType("application/octet-stream")
                .stream(stream, stream.available(), -1).build();
        minioClient.putObject(objectArgs);
        stream.close();
        return minioUrlExpose + bucketName + "/" + relativePath;
    }

    /**
     * 分享文件，有效时长2分钟
     *
     * @param fileName 文件名称
     * @return 分线连接
     * @author jiangtianyi
     * @since 1.0
     */
    public static String share(String fileName) {
        try {
            initMinio(minioUrl, minioName, minioPass);
            GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
                    .method(Method.GET).bucket(privateBucketName).object(fileName)
                    .expiry(2 * 60, TimeUnit.SECONDS).build();
            return minioClient.getPresignedObjectUrl(args).replaceAll(minioUrl, minioUrlExpose);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new JeecgBootException(e);
        }
    }

    /**
     * 文件分片
     *
     * @param parts    分片数量
     * @param filename 文件名
     */
    public static Map<String, Object> fragment(int parts, @Nonnull String filename) {
        initMinio(minioUrl, minioName, minioPass);
        MinIoClient minIoClient = new MinIoClient(minioClient);
        String bucket = getBucket();
        // update-begin-author:jiangtianyi date:20230504 for: 私有文件改造
        // 创建分片上传请求
        InitiateMultipartUploadResult initiateMultipartUploadResult = minIoClient.createMultipartUpload(bucket, null, filename, null, null).result();
        String[] parameters = IntStream.range(0, parts).mapToObj(index -> {
            Map<String, String> extraQueryParams = new HashMap<String, String>(1) {{
                this.put("partNumber", String.valueOf(index));
            }};
            // 预处理文件地址
            String url = minIoClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().method(Method.PUT).bucket(bucket).object(filename).expiry(1, TimeUnit.DAYS).extraQueryParams(extraQueryParams).build());
            return url.substring(url.indexOf("?") + 1);
        }).toArray(String[]::new);

        Map<String, Object> result = new HashMap<>();
        result.put("uploadId", initiateMultipartUploadResult.uploadId());
        result.put("parameters", parameters);
        return result;
    }

    /**
     * 上传分片文件
     *
     * @param uploadId   上传Id
     * @param filename   文件名
     * @param partNumber 分片序号
     * @param bytes      文件内容
     */
    public static UploadPartResponse upload(String uploadId, String filename, int partNumber, byte[] bytes) {
        log.info("File size {}", bytes.length);
        initMinio(minioUrl, minioName, minioPass);
        MinIoClient minIoClient = new MinIoClient(minioClient);
        String bucket = getBucket();
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = servletRequestAttributes.getRequest();
        return minIoClient.uploadPart(bucket, null, filename, bytes, bytes.length, uploadId, ++partNumber, null, null);
    }

    /**
     * 分片合并
     *
     * @param parts    分片数量
     * @param filename 文件名
     * @param uploadId 上传Id
     * @return 文件地址
     */
    @SneakyThrows
    public static String complete(int parts, @Nonnull String filename, @Nonnull String uploadId) {
        initMinio(minioUrl, minioName, minioPass);
        MinIoClient minIoClient = new MinIoClient(minioClient);
        String bucket = getBucket();
        // 查询分片文件
        ListPartsResponse listPartsResponse = minIoClient.listParts(bucket, null, filename, parts, null, uploadId, null, null);
        log.info("Should {}. Part size {}", parts, listPartsResponse.result().partList().size());
        // 完成分片上传, 合并文件
        minIoClient.completeMultipartUpload(bucket, null, filename, uploadId, listPartsResponse.result().partList().toArray(new Part[]{}), null, null);
        String objectName = getObjectName(filename, DateUtils.getDataString(new SimpleDateFormat("yyyyMMdd")));
        ObjectWriteResponse objectWriteResponse = minIoClient.copyObject(
                CopyObjectArgs.builder()
                        .source(CopySource.builder().bucket(bucket).object(filename).build())
                        .bucket(bucket)
                        .object(objectName)
                        .build());
        removeObject(bucket, filename);
        return isPrivate() ? filename : minioUrlExpose + bucket + "/" + objectName;
    }

    private static boolean isPrivate() {
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = servletRequestAttributes.getRequest();
        String privatelyOwned = request.getParameter("privatelyOwned");
        return StringUtils.isNotEmpty(privatelyOwned) && "1".equals(privatelyOwned);
    }

    private static String getBucket() {
        return isPrivate() ? privateBucketName : bucketName;
    }

    public static class MinIoClient extends MinioClient {

        protected MinIoClient(MinioClient client) {
            super(client);
        }

        /**
         * 创建分片上传请求
         *
         * @param bucketName       桶名称
         * @param region           区域
         * @param objectName       对象名
         * @param headers          请求头
         * @param extraQueryParams 查询参数
         * @return {@link CreateMultipartUploadResponse}
         */
        @SneakyThrows(value = {NoSuchAlgorithmException.class, InsufficientDataException.class, IOException.class, InvalidKeyException.class, ServerException.class, XmlParserException.class, ErrorResponseException.class, InternalException.class, InvalidResponseException.class})
        @Override
        protected CreateMultipartUploadResponse createMultipartUpload(String bucketName, String region, String objectName, Multimap<String, String> headers, Multimap<String, String> extraQueryParams) {
            return super.createMultipartUpload(bucketName, region, objectName, headers, extraQueryParams);
        }

        /**
         * 完成分片上传, 合并文件
         *
         * @param bucketName       桶名称
         * @param region           区域
         * @param objectName       对象名
         * @param uploadId         上传Id
         * @param parts            分片
         * @param extraHeaders     消息头
         * @param extraQueryParams 查询参数
         * @return {@link ObjectWriteResponse}
         */
        @SneakyThrows(value = {NoSuchAlgorithmException.class, InsufficientDataException.class, IOException.class, InvalidKeyException.class, ServerException.class, XmlParserException.class, ErrorResponseException.class, InternalException.class, InvalidResponseException.class})
        @Override
        protected ObjectWriteResponse completeMultipartUpload(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) {
            return super.completeMultipartUpload(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams);
        }

        /**
         * 查询分片文件
         *
         * @param bucketName       桶名称
         * @param region           区域
         * @param objectName       对象名
         * @param maxParts         最大分片
         * @param partNumberMarker 编号标记
         * @param uploadId         上传Id
         * @param extraHeaders     消息头
         * @param extraQueryParams 查询参数
         * @return {@link ListPartsResponse}
         */
        @SneakyThrows(value = {NoSuchAlgorithmException.class, InsufficientDataException.class, IOException.class, InvalidKeyException.class, ServerException.class, XmlParserException.class, ErrorResponseException.class, InternalException.class, InvalidResponseException.class})
        @Override
        protected ListPartsResponse listParts(String bucketName, String region, String objectName, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) {
            return super.listParts(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);
        }

        /**
         * 预处理文件地址
         *
         * @param args 预处理参数
         * @return 地址
         */
        @SneakyThrows(value = {ErrorResponseException.class, InsufficientDataException.class, InternalException.class, InvalidKeyException.class, InvalidResponseException.class, IOException.class, NoSuchAlgorithmException.class, XmlParserException.class, ServerException.class})
        @Override
        public String getPresignedObjectUrl(GetPresignedObjectUrlArgs args) {
            return super.getPresignedObjectUrl(args);
        }

        /**
         * 上传分片文件
         *
         * @param bucketName       桶名称
         * @param region           区域
         * @param objectName       对象名
         * @param data             文件流
         * @param length           长度
         * @param uploadId         上传Id
         * @param partNumber       分片编号
         * @param extraHeaders     消息头
         * @param extraQueryParams 查询参数
         * @return {@link UploadPartResponse}
         */
        @SneakyThrows(value = {NoSuchAlgorithmException.class, InsufficientDataException.class, IOException.class, InvalidKeyException.class, ServerException.class, XmlParserException.class, ErrorResponseException.class, InternalException.class, InvalidResponseException.class})
        @Override
        protected UploadPartResponse uploadPart(String bucketName, String region, String objectName, Object data, int length, String uploadId, int partNumber, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) {
            return super.uploadPart(bucketName, region, objectName, data, length, uploadId, partNumber, extraHeaders, extraQueryParams);
        }
    }
}
